package com.sluggard.mp.generator;


import com.baomidou.mybatisplus.generator.config.builder.CustomFile;
import com.sluggard.common.exception.BusinessException;
import com.sluggard.mp.mvc.BaseController;
import com.sluggard.mp.model.entity.IdEntity;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.text.CharSequenceUtil;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.config.TemplateType;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import com.baomidou.mybatisplus.generator.fill.Property;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Scanner;
import java.util.regex.Matcher;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

/**
 * <p>代码生成器二次封装</p>
 *
 * @author lizheng
 * @since 2024.4.17
 */
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class CodeGenerator {

    /**
     * 生成mvc代码
     *
     * @param property 代码生成器配置
     */
    public static void generate(@Valid GeneratorProperty property) {
        checkAndSetPropertyInteractively(property);
        setMybatisGenerator(property).execute();
    }

    /**
     * 交互式设置某些参数
     *
     * @param property 代码生成器配置
     */
    private static void checkAndSetPropertyInteractively(GeneratorProperty property) {
        String projectNamePrefix = property.getProjectNamePrefix();
        boolean invalidProjectName = CharSequenceUtil.isNotBlank(projectNamePrefix) && !property.getProjectName().startsWith(projectNamePrefix);
        if (invalidProjectName) {
            throw new BusinessException("项目名错误！后端项目名必须以" + projectNamePrefix + "开头");
        }
        Scanner sc = new Scanner(System.in);
        if (CharSequenceUtil.isBlank(property.getAuthor())) {
            System.out.println("> 请输入代码作者:");
            String author = Optional.ofNullable(sc.nextLine()).filter(CharSequenceUtil::isNotBlank).orElseThrow(() -> new BusinessException("作者名不能为空"));
            property.setAuthor(author);
        }
        if (property.getSkipBusinessDomain()) {
            property.setBusinessDomain(CharSequenceUtil.EMPTY);
        }
        if (CharSequenceUtil.isBlank(property.getBusinessDomain()) && !property.getSkipBusinessDomain()) {
            System.out.println("> 请输入业务模块名，可以为空");
            String businessDomain = sc.nextLine();
            property.setBusinessDomain(CharSequenceUtil.isBlank(businessDomain) ? CharSequenceUtil.EMPTY : businessDomain);
        }
        property.setBusinessDomain(property.getBusinessDomain().toLowerCase()).setPackagePrefix(property.getPackagePrefix().toLowerCase()).setProjectNamePrefix(property.getProjectNamePrefix().toLowerCase());
        System.out.println("===> 当前生成器配置:");
        System.out.println(property);
    }

    /**
     * 配置Mybatis Plus Generator
     *
     * @param property 代码生成器配置
     */
    private static FastAutoGenerator setMybatisGenerator(GeneratorProperty property) {
        String author = property.getAuthor();
        String packagePrefix = property.getPackagePrefix();
        String projectName = property.getProjectName();
        String businessDomain = property.getBusinessDomain();
        String projectNameSeparator = property.getProjectNameSeparator();
        String replacer = property.getProjectNameSeparatorReplacer();
        // 自定义变量配置，用于ftl模板中
        Map<String, Object> customConfig = new HashMap<>(1);
        customConfig.put("businessDomain", businessDomain);
        customConfig.put("superClassColumns", Arrays.asList("deleted","version", "createBy", "updateBy", "createTime", "updateTime"));


        PathParam pathParam = parseGeneratorProperty(property);
        CustomFile entityReq = getCustomFile(property, pathParam, packagePrefix, projectName, projectNameSeparator, replacer, businessDomain, customConfig);
        return FastAutoGenerator.create(property.getDataSourceBuilder())
                // 全局配置
                .globalConfig((scanner, builder) -> builder.author(author).disableOpenDir().enableSpringdoc())
                // 包配置
                .packageConfig((scanner, builder) -> builder
                        .parent(packagePrefix + GeneratorConstants.DOT + projectName.replace(property.getProjectNamePrefix(), CharSequenceUtil.EMPTY)
                                .replace(projectNameSeparator, replacer))
                        .entity(CharSequenceUtil.isBlank(businessDomain) ? GeneratorConstants.MODEL + GeneratorConstants.DOT + GeneratorConstants.ENTITY : businessDomain + GeneratorConstants.DOT + GeneratorConstants.MODEL + GeneratorConstants.DOT + GeneratorConstants.ENTITY)
                        .service(CharSequenceUtil.isBlank(businessDomain) ? GeneratorConstants.SERVICE : businessDomain + GeneratorConstants.DOT + GeneratorConstants.SERVICE)
                        .serviceImpl(CharSequenceUtil.isBlank(businessDomain) ? GeneratorConstants.SERVICE_DOT_IMPL : businessDomain + GeneratorConstants.DOT + GeneratorConstants.SERVICE_DOT_IMPL)
                        .mapper(CharSequenceUtil.isBlank(businessDomain) ? GeneratorConstants.MAPPER : businessDomain + GeneratorConstants.DOT + GeneratorConstants.MAPPER)
                        .controller(CharSequenceUtil.isBlank(businessDomain) ? GeneratorConstants.CONTROLLER : businessDomain + GeneratorConstants.DOT + GeneratorConstants.CONTROLLER)
                        .pathInfo(buildPathInfo(pathParam)))
                // 策略配置
                .strategyConfig((scanner, builder) -> {
                    builder.addInclude(getTables(scanner.apply("> 请输入表名，多个英文逗号分隔，所有输入 all:")))
                            .controllerBuilder()
                            .superClass(property.controllerSuperClass)
                            // 使用@RestController
                            .enableRestStyle()
                            // 驼峰名转换
                            .enableHyphenStyle()
                            // service类名模板
                            .serviceBuilder()
                            .mapperBuilder()
                            .enableBaseColumnList()
                            .enableBaseResultMap()
                            .entityBuilder()
                            // 使用lombok注解
                            .enableLombok()
                            .superClass(property.entitySuperClass)
                            // 链式调用
                            .enableChainModel()
                            // entity类名模板
                            .formatFileName("%sEntity")
                            // Boolean类型字段移除is前缀
                            .enableRemoveIsPrefix()
                            // 生成mybatis字段注解
                            .enableTableFieldAnnotation();

                    if (property.getOverwriteFiles() != null && property.getOverwriteFiles()) {
                        builder.controllerBuilder().enableFileOverride();
                        builder.serviceBuilder().enableFileOverride();
                        builder.mapperBuilder().enableFileOverride();
                        builder.entityBuilder().enableFileOverride();
                    }
                    if (property.getEntitySuperClass() != null) {
                        builder.entityBuilder().superClass(property.getEntitySuperClass());
                    }
                    if (CollUtil.isNotEmpty(property.getTablePrefixList())) {
                        builder.addTablePrefix(property.getTablePrefixList().toArray(new String[]{}));
                    }
                    // 自动填充创建时间，修改时间
                    builder.entityBuilder().addTableFills(
                                    new Property("create_time", FieldFill.INSERT),
                                    new Property("deleted", FieldFill.INSERT),
                                    new Property("version", FieldFill.INSERT_UPDATE),
                                    new Property("update_time", FieldFill.INSERT_UPDATE)
                            )
                            .build();
                })
                // 模板配置
                .templateEngine(new FreemarkerTemplateEngine()).templateConfig((string, builder) -> {
                    builder
                            .disable(property.getDisableConfig().disableTemplateTypes)
                            .entity("tpl/entity.java")
                            .controller("tpl/controller.java")
                            .serviceImpl("tpl/serviceImpl.java");
                    if (property.disableConfig.disableController != null && property.disableConfig.disableController) {
                        builder.controller(CharSequenceUtil.EMPTY);
                    }
                }).injectionConfig(builder -> builder
                        .customMap(customConfig)
                        .customFile(entityReq)
                );
    }

    private static CustomFile getCustomFile(GeneratorProperty property,
                                            PathParam pathParam,
                                            String packagePrefix,
                                            String projectName,
                                            String projectNameSeparator,
                                            String replacer,
                                            String businessDomain,
                                            Map<String, Object> customConfig) {
        String parent = packagePrefix + GeneratorConstants.DOT + projectName.replace(property.getProjectNamePrefix(), CharSequenceUtil.EMPTY)
                .replace(projectNameSeparator, replacer);
        String reqPkg = parent + GeneratorConstants.DOT + (CharSequenceUtil.isBlank(businessDomain) ? GeneratorConstants.MODEL + GeneratorConstants.DOT + GeneratorConstants.REQ : businessDomain + GeneratorConstants.DOT + GeneratorConstants.MODEL + GeneratorConstants.DOT + GeneratorConstants.REQ);
        CustomFile entityReq = new CustomFile.Builder()
                .filePath(pathParam.businessModuleJavaPath)
                .fileName(".java")
                .formatNameFunction(tableInfo -> tableInfo.getEntityName().replace("Entity", "") + "Req")
                .templatePath("/templates/entity_req.java.ftl")
                .enableFileOverride()
                .packageName(reqPkg)
                .build();
        customConfig.put("reqPkg", reqPkg);
        return entityReq;
    }

    /**
     * 创建不存在的路径或文件
     *
     * @param path 需要创建的路径
     */
    private static void createFileOrPath(Path path) {
        if (path == null) {
            return;
        }
        if (Files.notExists(path)) {
            try {
                Files.createDirectories(path.getParent());
                Files.createFile(path);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 获取表名
     *
     * @param tables 逗号分割的表名，可以为"all"
     * @return 表名列表，如果为"all"，则返回空列表
     */
    private static List<String> getTables(String tables) {
        return "all".equals(tables) ? Collections.emptyList() : Arrays.asList(tables.split(","));
    }

    /**
     * 自定义文件生成路径
     *
     * @param pathParam
     * @return 文件对应的路径map
     */
    public static Map<OutputFile, String> buildPathInfo(PathParam pathParam) {
        Map<OutputFile, String> pathInfo = new EnumMap<>(OutputFile.class);

        String businessPkgPath = pathParam.businessModuleJavaPath + (CharSequenceUtil.isBlank(pathParam.businessDomain) ? pathParam.packagePathPrefix + File.separator : pathParam.packagePathPrefix + File.separator + pathParam.businessDomain + File.separator);
        String entityPkgPath = businessPkgPath + File.separator + GeneratorConstants.MODEL + File.separator + GeneratorConstants.ENTITY + File.separator;
        String mapperPath = businessPkgPath + File.separator + GeneratorConstants.MAPPER + File.separator;
        String mapperXmlPath = pathParam.businessModuleResourcePath + GeneratorConstants.MAPPER;
        String controllerPath = businessPkgPath + File.separator + GeneratorConstants.CONTROLLER + File.separator;
        String servicePath = businessPkgPath + File.separator + GeneratorConstants.SERVICE + File.separator;
        String serviceImplPath = businessPkgPath + File.separator + GeneratorConstants.SERVICE + File.separator + GeneratorConstants.IMPL + File.separator;
        pathInfo.put(OutputFile.controller, controllerPath);
        pathInfo.put(OutputFile.service, servicePath);
        pathInfo.put(OutputFile.serviceImpl, serviceImplPath);
        pathInfo.put(OutputFile.entity, entityPkgPath);
        pathInfo.put(OutputFile.mapper, mapperPath);
        pathInfo.put(OutputFile.xml, mapperXmlPath);
        return pathInfo;
    }

    private static PathParam parseGeneratorProperty(GeneratorProperty property) {
        String userDir = System.getProperty(GeneratorConstants.USER_DIR);
        if (StringUtils.isNoneBlank(property.subDir)) {
            userDir = userDir + (property.subDir.startsWith("/") ? property.subDir : File.separator + property.subDir);
        }
        String projectName = property.getProjectName();
        String projectNameForModule = property.getIsMultiModuleProject() ? projectName.replace(property.getProjectNamePrefix(), CharSequenceUtil.EMPTY) : CharSequenceUtil.EMPTY;
        String projectNameForPackage = projectName.replace(property.getProjectNamePrefix(), CharSequenceUtil.EMPTY).replace(property.getProjectNameSeparator(), property.projectNameSeparatorReplacer);
        String businessDomain = property.getBusinessDomain();
        String packagePathPrefix = property.getPackagePrefix().replace(GeneratorConstants.DOT, Matcher.quoteReplacement(File.separator)) + File.separator + projectNameForPackage.replace(GeneratorConstants.DOT, Matcher.quoteReplacement(File.separator));
        String modulePathPrefix = userDir + File.separator + projectNameForModule.replace(GeneratorConstants.DOT, Matcher.quoteReplacement(File.separator));
        String businessModuleJavaPath = modulePathPrefix + File.separator + GeneratorConstants.SRC + File.separator + GeneratorConstants.MAIN + File.separator + GeneratorConstants.JAVA + File.separator;
        String businessModuleResourcePath = modulePathPrefix + File.separator + GeneratorConstants.SRC + File.separator + GeneratorConstants.MAIN + File.separator + GeneratorConstants.RESOURCES + File.separator;
        return new PathParam().setUserDir(userDir).setProjectName(projectName).setProjectNameForPackage(projectNameForPackage).setProjectNameForModule(projectNameForModule).setBusinessDomain(businessDomain).setPackagePathPrefix(packagePathPrefix).setModulePathPrefix(modulePathPrefix).setBusinessModuleJavaPath(businessModuleJavaPath).setBusinessModuleResourcePath(businessModuleResourcePath);
    }

    /**
     * 代码生成器配置类
     */
    @Data
    @Accessors(chain = true)
    @AllArgsConstructor
    public static class GeneratorProperty {

        /**
         * 【必须】数据源配置Builder
         */
        @NotNull
        private DataSourceConfig.Builder dataSourceBuilder;

        /**
         * 【可选】禁用配置，默认不会生成controller
         */
        private DisableConfig disableConfig;

        /**
         * 【可选】子目录，默认为空
         */
        private String subDir;
        /**
         * 【可选】包名前缀，默认com.sluggard
         */
        private String packagePrefix;
        /**
         * 【可选】项目名前缀，用于校验项目名是否正确，默认为空
         */
        private String projectNamePrefix;
        /**
         * 【可选】代码作者，可通过交互式设置
         */
        private String author;
        /**
         * 【必须】项目名
         */
        @NotBlank
        private String projectName;
        /**
         * 【可选】业务领域名，可通过交互式设置，如果配置了该项，则mvc层都创建在当前业务包内，否则mvc层是平铺的
         */
        private String businessDomain;
        /**
         * 生成文件时是否覆盖已有的文件
         */
        private Boolean overwriteFiles;
        /**
         * DO实体类的父类
         * IdEntity
         * OperatorAndTimeEntity
         * OperatorEntity
         * StandardAuditEntity
         * TimeEntity
         * VersionEntity
         */
        private Class<?> entitySuperClass;
        /**
         * DO实体类的父类
         * IdEntity
         * OperatorAndTimeEntity
         * OperatorEntity
         * StandardAuditEntity
         * TimeEntity
         * VersionEntity
         */
        private Class<?> controllerSuperClass;
        /**
         * 项目名分割符替代品，用于转换项目名为包名，默认替换为空字符串
         */
        private String projectNameSeparatorReplacer;
        /**
         * 【可选】项目名分割符，默认为横杠-
         */
        private String projectNameSeparator;
        /**
         * 【可选】是否是多模块项目，默认是
         */
        private Boolean isMultiModuleProject;
        /**
         * 【可选】跳过业务域配置，默认false
         */
        private Boolean skipBusinessDomain;
        /**
         * 【可选】表名前缀，如果指定，生成的类会去掉前缀
         */
        private List<String> tablePrefixList;

        public GeneratorProperty() {
            this.packagePrefix = "com.sluggard";
            this.projectNamePrefix = "";
            DisableConfig defaultDisable = new DisableConfig();
            defaultDisable.setDisableController(true);
            this.disableConfig = defaultDisable;
            this.projectNameSeparatorReplacer = CharSequenceUtil.EMPTY;
            this.projectNameSeparator = GeneratorConstants.HYPHEN;
            this.isMultiModuleProject = true;
            this.skipBusinessDomain = false;
            this.entitySuperClass = IdEntity.class;
            this.controllerSuperClass = BaseController.class;
        }
    }

    @Data
    @Accessors(chain = true)
    @NoArgsConstructor
    @AllArgsConstructor
    private static class PathParam {

        private String userDir;
        private String projectName;
        private String projectNameForPackage;
        private String projectNameForModule;
        private String businessDomain;
        private String packagePathPrefix;
        private String modulePathPrefix;
        private String businessModuleJavaPath;
        private String businessModuleResourcePath;
    }

    @Data
    @Accessors(chain = true)
    @NoArgsConstructor
    @AllArgsConstructor
    public static class DisableConfig {

        /**
         * 禁用模板
         */
        private TemplateType[] disableTemplateTypes;

        /**
         * 禁止生成controller，默认true
         */
        private Boolean disableController;
    }
}
